Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix partial placeholder . in middle of call chain #1450

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

edemaine
Copy link
Collaborator

Fixes #1449

@STRd6
Copy link
Contributor

STRd6 commented Oct 10, 2024

I have mixed feelings about this one... I think it could be handy to lift above trailing call expressions much lifting above.

It's easy enough to short circuit the chain by adding parens:

(f(.))(b)

But I don't know how we'd add in the chain easily if we switch it up. I could be convinced but I'd need to see some more examples.

@edemaine
Copy link
Collaborator Author

I can't think of many situations where I want to do multiple calls in a chain. Currying is the obvious example. The current behavior might be useful when currying:

add := (x) => (y) => x+y
leftSpace := add(' ')(.) // works with or without PR
rightSpace := add(.)(' ') // relies on current behavior, not PR

On the other hand, I think partial placeholders are designed to replace the need for currying in the first place:

add := (x, y) => x+y
leftSpace := add(' ', .) // works with or without PR
rightSpace := add(., ' ') // works with or without PR

Here are some arguments for why I think the PR behavior is better:

  • Ability to specify an argument later, as in Arguments placeholders for callbacks #837 (comment) : setTimeout(., 1000) => // here's the callback
  • Intuitively, I think we always envisioned . (or ...) as being placeholders (blanks) for this specific call. To do the call, we of course need to grab the function being called (all calls to the left in the chain), but I think it's counterintuitive to also grab things to the right of this call...
  • Related, it's easier to explain. To describe the current behavior, the documentation would have to be changed to the following (added text), which seems harder to understand:

    if you use . within a chain of function calls, that call chain gets wrapped in a one-argument function and . gets replaced by that argument

Admittedly, the examples are thin. If you can think of others to help decide, I'd love to see them!

@STRd6
Copy link
Contributor

STRd6 commented Oct 12, 2024

Thanks for the explanation, I'm mostly convinced, however there is one other situation I think needs to be considered:

// Currently .normalize() is applied inside the wrapper
y := f(.).normalize()
// Alternative that works with or without this change
x := & |> f |> .normalize()

Since there are very few properties of a function that are useful to access it is more generally useful to have the member expressions applied inside.

By a similar argument we may even want to expand to the left further as well:

// currently not very useful (await and binary exps are not consumed by the placeholder function)
y := 1 + await f .
---
const y = async $ => 1 + await $

We could even extend to handle:

// currently needs to use `&` for the placeholder, `.` is a parse error
f := 1 + .

If we do . and & would be synonymous in this case ( . could expand up to the enclosing function, statement, or declaration level)

@edemaine
Copy link
Collaborator Author

edemaine commented Oct 12, 2024

Hmm, nice examples. I'm actually surprised that await isn't already brought inside the function, as other unary prefix operators (e.g. -) are.

I guess this suggests an alternative interpretation for . which might be more useful:

  • & lifts to just inside the nearest function call, assignment, pipeline, return, or yield
  • . lifts to outside the nearest function call, and from there, lifts further to just inside the nearest function call, assignment, pipeline, return, or yield

The bold part is the new part compared to the current definition.
I'm almost tempted to call this new proposal && (with more &s to pull outside successive function call levels).
(Related, I've found . to always look a little gross when used in some contexts, like (.).foo (thankfully ..foo doesn't work).)

Maybe we could stick to . for partial application (just omitted arguments in functions), generalize it to handle multiple arguments, and introduce && to lift further up?

@STRd6
Copy link
Contributor

STRd6 commented Oct 13, 2024

  • & lifts to just inside the nearest function call, assignment, pipeline, return, or yield
  • . lifts to outside the nearest function call, and from there, lifts further to just inside the nearest function call, assignment, pipeline, return, or yield

I think this is a good interpretation with the possible addition of statement and parenthesized expression as additional places that the lifting halts.

I'm not very much of a fan of && for lifting additional levels, mainly because at &&& it would already become unreasonable to me.

For multiple argument partial application I've been considering \1, \2, ...

@edemaine
Copy link
Collaborator Author

edemaine commented Oct 14, 2024

Ooh, parenthesized expression sounds like a nice place to stop lifting! Currently the lift just rips through that, which I think is counterintuitive and makes it hard to stop the lift (I usually use a do wrapper).

(We already stop at statements, as previously discussed, but it should be documented!)

I'm still uncertain whether . is intuitive for going up a "full second level"... maybe. I still learn toward some other notation, and keeping . for the original intent of lifting just outside a call... Related:

  • Do you think it's worth trying to support "delayed arguments" like setTimeout(., 1000) => callback with some notation (. or otherwise)?
  • I think that, while f(.).normalize() is cool and useful, this specific notation isn't especially intuitive... (though maybe it's just my mental model of . needs to change)
  • I expect f(.) to mean $ => f($) i.e. a one-argument version of . (useful in e.g. array.map f(.) to shield f from the other arguments). Admittedly it's not that useful to call a method on a function, but it's still surprising...

@STRd6
Copy link
Contributor

STRd6 commented Oct 14, 2024

The delayed arguments seem to work ok already with this construct:

(setTimeout ., 100) =>
  console.log(x)

The way I think about . is less about currying / managing arguments and more about creating functions from a slot inside an expression. Sort of as a utility that should fit into |> so that single argument pipelines and single argument functions are very easy to create and stitch together even if they span complex expressions, await, etc.

(sorry for the close/reopen on this issue, fat fingered ctrl+enter when trying to paste code into the example)

@STRd6 STRd6 closed this Oct 14, 2024
@STRd6 STRd6 reopened this Oct 14, 2024
edemaine added a commit that referenced this pull request Oct 14, 2024
Cherry-pick from #1450
@edemaine edemaine marked this pull request as draft October 15, 2024 14:54
@edemaine
Copy link
Collaborator Author

I just found myself writing the following code (here & is of type {about?: string, code: string}):

transpileds.map((&.about ?? '')# + &.code#)

This wouldn't work if we stop & at a parenthesized expression. Which behavior do we want?

@STRd6
Copy link
Contributor

STRd6 commented Oct 21, 2024

Good point, maybe we should continue through parentheses. I can see it coming up in math equations pretty regularly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

. placeholder lifts outside long string of calls
2 participants